# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import ConfigParser
from cStringIO import StringIO
import os
import socket
import sys
import threading

import coverage

# This is instead of a contextmanager because it causes old pylints to crash :(
class _Cover(object):

  # Counter and associated lock for assigning unique IDs for coverage filename
  # suffixes. The coverage library's automatic suffix generation uses 6 digit
  # random integer and we were seeing collisions and files getting clobbered.
  # TODO(estaab): Fix this upstream in coverage.
  current_id_lock = threading.Lock()
  current_id = 0

  @classmethod
  def unique_id(cls):
    """Returns a unique integer for this process."""
    with cls.current_id_lock:
      cls.current_id += 1
      return cls.current_id

  def __init__(self, enabled, maybe_kwargs):
    self.enabled = enabled
    self.kwargs = maybe_kwargs or {}
    self.c = None

  def update(self, **kwargs):
    """Get a new _Cover object with updated parameters """
    new_kwargs = self.kwargs
    if self.enabled:
      new_kwargs = new_kwargs.copy()
      new_kwargs.update(kwargs)
    return _Cover(self.enabled, new_kwargs)

  def __enter__(self):
    if self.enabled:
      if self.c is None:
        kwargs = self.kwargs.copy()
        kwargs['data_suffix'] = "%s.%s.%s" % (
            socket.gethostname(), os.getpid(), self.unique_id())
        self.c = coverage.coverage(**kwargs)
        self.c._warn_no_data = False
      self.c.start()

  def __exit__(self, *_):
    if self.enabled:
      self.c.stop()
      self.c.save()


class CoverageContext(object):
  def __init__(self, cover_branches, html_report, enabled=True,
               coveragerc=None):
    """
    Args:
      cover_branches (boolean): if True, also collect branche coverage
      html_report (str): path to a directory where to write an html coverage
        report. If None, no report is created.
      enabled (boolean): activate or deactivate coverage as a whole.
      coveragerc (str): path to a coverage configuration file.
        See also http://nedbatchelder.com/code/coverage/config.html
    """
    self.opts = None
    self.cov = None
    self.enabled = enabled
    self.expected_coverage_min = 100

    self.html_report = html_report

    if enabled:
      config_file = os.path.abspath(coveragerc or '.coveragerc')
      if os.path.isfile(config_file):
        parser = ConfigParser.ConfigParser()
        parser.read([config_file])
        if parser.has_option('expect_tests', 'expected_coverage_min'):
          value = parser.get('expect_tests', 'expected_coverage_min')
          try:
            self.expected_coverage_min = int(value)
          except ValueError:
            print >> sys,stderr, (
              'Invalid value for expected_coverage_min in %s.'
              ' Value should be an integer got %s', (config_file, value))

      self.opts = {
        'data_file': os.path.abspath('.expect_tests_coverage'),
        'branch': cover_branches,
        'config_file': config_file,
      }
      self.cov = coverage.coverage(**self.opts)
      self.cov.erase()

  def cleanup(self):
    if self.enabled:
      self.cov.combine()

  def report(self, verbose):
    fail = False

    if self.enabled:
      if self.html_report:
        self.cov.html_report(directory=self.html_report)

      outf = StringIO()
      coverage_percent = self.cov.report(file=outf)
      fail = coverage_percent < self.expected_coverage_min
      summary = outf.getvalue().replace('%- 15s' % 'Name', 'Coverage Report', 1)
      if verbose:
        print
        print summary
      elif fail:
        print
        lines = summary.splitlines()
        lines[2:-2] = [l for l in lines[2:-2]
                       if not l.strip().endswith('100%')]
        print '\n'.join(lines)
        print
        print ('FATAL: Test coverage is %.1f%%, target is %d%%.' %
               (coverage_percent, self.expected_coverage_min))
      else:
        print ('Code coverage is %.1f%%, target is %d%%.' %
               (coverage_percent, self.expected_coverage_min))
    return not fail

  def create_subprocess_context(self):
    # Can't have this method be the contextmanager because otherwise
    # self (and self.cov) will get pickled to the subprocess, and we don't want
    # that :(
    return _Cover(self.enabled, self.opts)
